﻿/* Der Autor des nachfolgenden Codes ist 'dN!3L'
 * Der Forumspost zum Code ist unter folgender URL zu finden:
 * http://www.mycsharp.de/wbb2/thread.php?threadid=52731&hilight=DynamicAccessor
 */

using System;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.Serialization;
using System.Text.RegularExpressions;
 
/// <summary>
/// Stellt Methoden bereit, um Objekte zu erzeugen, die den schnellen Zugriff (ohne Reflection) auf Felder/Properties ermöglichen
/// </summary>
public abstract class DynamicFieldAccessor
{
    // Cache zum Speichern der bereits erzeugten "dynamischen Feldzugreifer"
    private static Dictionary<bool,Dictionary<MemberInfo,IDynamicFieldAccessor>> cache = new Dictionary<bool,Dictionary<MemberInfo,IDynamicFieldAccessor>>();

    // MethodInfos, die oft benutzt werden
    private static readonly MethodInfo typeGetTypeFromHandleMethodInfo = typeof(Type).GetMethod("GetTypeFromHandle");
    private static readonly MethodInfo objectGetTypeMethodInfo = typeof(object).GetMethod("GetType");
    private static readonly MethodInfo typeIsAssignableFromMethodInfo = typeof(Type).GetMethod("IsAssignableFrom");
    private static readonly MethodInfo convertChangeTypeMethodInfo = typeof(Convert).GetMethod("ChangeType",new Type[]{typeof(object),typeof(Type)});
    private static readonly MethodInfo objectToStringMethodInfo = typeof(object).GetMethod("ToString");
    private static readonly MethodInfo enumParseMethodInfo = typeof(Enum).GetMethod("Parse",new Type[]{typeof(Type),typeof(string)});
    private static readonly MethodInfo nullableGetUnderlyingTypeMethodInfo = typeof(Nullable).GetMethod("GetUnderlyingType");

    /// <summary>
    /// Konstruktor
    /// </summary>
    static DynamicFieldAccessor()
    {
        // Felder im Cache anlegen
        DynamicFieldAccessor.cache.Add(true,new Dictionary<MemberInfo,IDynamicFieldAccessor>());
        DynamicFieldAccessor.cache.Add(false,new Dictionary<MemberInfo,IDynamicFieldAccessor>());
    }

    /// <summary>
    /// Erzeugt eine neue IDynamicFieldAccessor-Instanz
    /// </summary>
    /// <param name="memberInfo">das Feld/Property, auf das zugegriffen werden soll</param>
    /// <param name="enableSetterTypeChange">true, falls eine (eventuelle) Typkonvertierung beim Setzen von Werten vorgenommen werden soll</param>
    /// <returns>eine neue IDynamicFieldAccessor-Instanz zum Zugriff auf das übergebene Feld/Property</returns>
    public static IDynamicFieldAccessor Create(MemberInfo memberInfo,bool enableSetterTypeChange)
    {
        if (!(memberInfo is PropertyInfo || memberInfo is FieldInfo))
            throw new ArgumentException(memberInfo+" ist kein Feld oder Property.");
        else
            // Instanz aus dem Cache zurückgeben
            if (DynamicFieldAccessor.cache[enableSetterTypeChange].ContainsKey(memberInfo))
                return DynamicFieldAccessor.cache[enableSetterTypeChange][memberInfo];
            // neue Instanz erzeugen und im Cache speichern
            else
            {
                IDynamicFieldAccessor fieldAccessor = DynamicFieldAccessor.create(memberInfo,enableSetterTypeChange);
                DynamicFieldAccessor.cache[enableSetterTypeChange].Add(memberInfo,fieldAccessor);
                return fieldAccessor;
            }
    }

    /// <summary>
    /// Erzeugt eine neue IDynamicFieldAccessor-Instanz
    /// </summary>
    /// <param name="memberInfo">das Feld/Property, auf das zugegriffen werden soll</param>
    /// <param name="enableSetterTypeChange">true, falls eine (eventuelle) Typkonvertierung beim Setzen von Werten vorgenommen werden soll</param>
    /// <returns>eine neue IDynamicFieldAccessor-Instanz zum Zugriff auf das übergebene Feld/Property</returns>
    private static IDynamicFieldAccessor create(MemberInfo memberInfo,bool enableSetterTypeChange)
    {
        // Pipeline AssemblyBuilder->ModuleBuilder->TypeBuilder

        string name = Guid.NewGuid().ToString();
        AssemblyName assemblyName = new AssemblyName(name);
        AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly (assemblyName,AssemblyBuilderAccess.Run);
        ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(name);
        // einen neuen TypeBuilder erzeugen. Falls das Feld/Property nicht public ist, muss vom reflektierten Typ geerbt werden, damit Zugriff erhalten werden kann
        TypeBuilder typeBuilder = (DynamicFieldAccessor.mustInherit(memberInfo)) ? moduleBuilder.DefineType(name,TypeAttributes.Public,memberInfo.ReflectedType) : moduleBuilder.DefineType(name,TypeAttributes.Public);


        // der neue Typ implementiert die Schnittstelle IDynamicFieldAccessor; die hat eine Methode "Get" und eine Methode "Set", welche implementiert werden müssen
        typeBuilder.AddInterfaceImplementation(typeof(IDynamicFieldAccessor));
        DynamicFieldAccessor.implementGetMethod(typeBuilder,memberInfo);
        DynamicFieldAccessor.implementSetMethod (typeBuilder,memberInfo,enableSetterTypeChange);


        // den Typen erzeugen
        Type resultType = typeBuilder.CreateType();

        // neue Instanz erzeugen -> dabei wird Konstruktor nicht aufgerufen (braucht nicht; hätte bei Spezialisierungen, um an protected Member zu kommen, auch Nebeneffekte)
        IDynamicFieldAccessor result = (IDynamicFieldAccessor)FormatterServices.GetUninitializedObject(resultType);
        return result;
    }

    /// <summary>
    /// Prüft, ob ein neuer Typ vom Typ, der das Property/Feld enthält, ableiten muss, um an das Property/Feld zu kommen, da dies nicht public ist
    /// </summary>
    /// <param name="memberInfo">der Member, der untersucht werden soll</param>
    /// <returns>true, falls abgeleitet werden muss</returns>
    private static bool mustInherit(MemberInfo memberInfo)
    {
        PropertyInfo propertyInfo = memberInfo as PropertyInfo;
        FieldInfo fieldInfo = memberInfo as FieldInfo;

        // Es muss abgeleitet werden, wenn Getter oder Setter des Properties oder das Feld nicht public ist.
        // Getter/Setter/Feld müssen natürlich auch protected sein

        // wenn es ein Property ist
        if (propertyInfo!=null)
        {
            MethodInfo getMethod = propertyInfo.GetGetMethod(true);
            if (getMethod!=null)
            {
                if ((getMethod.Attributes & MethodAttributes.Family)!=MethodAttributes.Family)
                    throw new ArgumentException("'"+memberInfo+"' muss public oder protected sein.");
                if ((getMethod.Attributes & MethodAttributes.Public)!=MethodAttributes.Public)
                    return true;
            }

            MethodInfo setMethod = propertyInfo.GetSetMethod(true);
            if (setMethod!=null)
            {
                if ((setMethod.Attributes & MethodAttributes.Family)!=MethodAttributes.Family)
                    throw new ArgumentException("'"+memberInfo+"' muss public oder protected sein.");
                if ((setMethod.Attributes & MethodAttributes.Public)!=MethodAttributes.Public)
                    return true;
            }
        }
        // wenn es ein Feld ist
        else
        {
            if ((fieldInfo.Attributes & FieldAttributes.Family)!=FieldAttributes.Family)
                throw new ArgumentException("'"+memberInfo+"' muss public oder protected sein.");
            if ((fieldInfo.Attributes & FieldAttributes.Public)!=FieldAttributes.Public)
                return true;
        }

        return false;
    }

    /// <summary>
    /// Implementiert die "object Get(object target)"-Methode von IDynamicFieldAccessor
    /// </summary>
    /// <param name="typeBuilder">der zu verwendende TypeBuilder</param>
    /// <param name="memberInfo">das Feld/Property, auf das ausgelesen werden soll</param>
    private static void implementGetMethod(TypeBuilder typeBuilder,MemberInfo memberInfo)
    {
        PropertyInfo propertyInfo = memberInfo as PropertyInfo;
        FieldInfo fieldInfo = memberInfo as FieldInfo;

        // neue Methode erstellen und die Methode aus der Schnittstellendeklaration damit überschreiben
        MethodInfo methodInfo = typeof(IDynamicFieldAccessor).GetMethod("Get");
        MethodBuilder methodBuilder = typeBuilder.DefineMethod(methodInfo.Name,(MethodAttributes) (methodInfo.Attributes-MethodAttributes.Abstract),methodInfo.ReturnType,new Type[] { typeof(object) });
        typeBuilder.DefineMethodOverride(methodBuilder,methodInfo);

        // IL-Generator erzeugen
        ILGenerator ilGenerator = methodBuilder.GetILGenerator();

        if (propertyInfo!=null && propertyInfo.GetGetMethod(true)==null)
        {
            // wenn es ein Property ist und kein Getter vorhanden ist, Exception werfen
            ilGenerator.Emit(OpCodes.Ldstr,"Property '"+memberInfo+"' hat keinen Getter.");
            ilGenerator.Emit(OpCodes.Newobj,typeof(NotSupportedException).GetConstructor(new Type[] { typeof(string) }));
            ilGenerator.Emit(OpCodes.Throw);
        }
        // Property-/Feldzugriff implementieren
        else
        {
            // lokale Variable
            ilGenerator.DeclareLocal(typeof(object));

            // übergebenes Objekt laden und zum casten
            ilGenerator.Emit(OpCodes.Ldarg_1);
            ilGenerator.Emit(OpCodes.Castclass,memberInfo.ReflectedType);

            if (propertyInfo!=null)
                // Property auslesen
                ilGenerator.Emit(OpCodes.Callvirt,propertyInfo.GetGetMethod(true));
            else
                // Feld auslesen
                ilGenerator.Emit(OpCodes.Ldfld,fieldInfo);

            // den gelesenen Property-/Feldwert boxen
            Type memberType = (propertyInfo!=null) ? propertyInfo.PropertyType : fieldInfo.FieldType;
            if (memberType.IsValueType)
                ilGenerator.Emit(OpCodes.Box,memberType);

            // gelesenen Property-/Feldwert zurückgeben
            ilGenerator.Emit(OpCodes.Ret);
        }
    }

    /// <summary>
    /// Implementiert die "void Set(object target,object value);"-Methode von IDynamicFieldAccessor
    /// </summary>
    /// <param name="typeBuilder">der zu verwendende TypeBuilder</param>
    /// <param name="memberInfo">das Feld/Property, auf das gesetzt werden soll</param>
    /// <param name="changeType">true, falls eine (eventuelle) Typkonvertierung beim Setzen von Werten vorgenommen werden soll</param>
    private static void implementSetMethod(TypeBuilder typeBuilder,MemberInfo memberInfo,bool changeType)
    {
        PropertyInfo propertyInfo = memberInfo as PropertyInfo;
        FieldInfo fieldInfo = memberInfo as FieldInfo;
        Type memberType = (propertyInfo!=null) ? propertyInfo.PropertyType : fieldInfo.FieldType;    // Zieltyp

        // neue Methode erstellen und die Methode aus der Schnittstellendeklaration damit überschreiben
        MethodInfo methodInfo = typeof(IDynamicFieldAccessor).GetMethod("Set");
        MethodBuilder methodBuilder = typeBuilder.DefineMethod(methodInfo.Name,(MethodAttributes) (methodInfo.Attributes-MethodAttributes.Abstract),methodInfo.ReturnType,new Type[] { typeof(object),typeof(object) });
        typeBuilder.DefineMethodOverride(methodBuilder,methodInfo);

        // IL-Generator erzeugen
        ILGenerator ilGenerator = methodBuilder.GetILGenerator();

        // Sprungziele definieren
        Label labelSetConverted = ilGenerator.DefineLabel();
        Label labelSetDirect = ilGenerator.DefineLabel();

        if (propertyInfo!=null && propertyInfo.GetSetMethod(true)==null)
        {
            // wenn es ein Property ist und kein Setter vorhanden ist, Exception werfen
            ilGenerator.Emit(OpCodes.Ldstr,"Property '"+memberInfo+"' hat keinen Setter.");
            ilGenerator.Emit(OpCodes.Newobj,typeof(NotSupportedException).GetConstructor(new Type[]{typeof(string)}));
            ilGenerator.Emit(OpCodes.Throw);
        }
        // Property-/Feldzugriff implementieren
        else
        {
            // wenn gewünscht, die TypPRÜFUNG implementieren
            if (changeType)
            {
                // lokale Variable
                ilGenerator.DeclareLocal(typeof(bool));

                // den zu setzende Wert laden; {value is MemberType}-Vergleich machen; falls erfolgreich, zum direkt ohne Konvertieren setzen gehen
                ilGenerator.Emit(OpCodes.Ldarg_2);
                ilGenerator.Emit(OpCodes.Isinst,(!memberType.IsEnum) ? memberType : typeof(int));
                ilGenerator.Emit(OpCodes.Brtrue_S,labelSetDirect);

                // is-Vergleich ist nicht erfolgreich: bei Enumtypen zum Konvertieren gehen...
                if (memberType.IsEnum)
                    ilGenerator.Emit(OpCodes.Br_S,labelSetConverted);
                // ... ansonsten noch Testen, ob der Wert nicht doch zuweisbar ist
                else
                {
                    // {if (typeof(MemberType).IsAssignableFrom(value.GetType()))}
                    ilGenerator.Emit(OpCodes.Ldtoken,memberType);
                    ilGenerator.Emit(OpCodes.Call,typeGetTypeFromHandleMethodInfo);
                    ilGenerator.Emit(OpCodes.Ldarg_2);
                    ilGenerator.Emit(OpCodes.Callvirt,objectGetTypeMethodInfo);
                    ilGenerator.Emit(OpCodes.Callvirt,typeIsAssignableFromMethodInfo);

                    // wenn der Test nicht erfolgreich war, zum Konvertieren gehen...
                    ilGenerator.Emit(OpCodes.Brfalse_S,labelSetConverted);
                    // ... ansonsten kommt jetzt folgend die Zuweisung ohne Konvertieren
                }

                ilGenerator.MarkLabel(labelSetDirect);
            }

            // Zielobjekt laden; dieses casten; Wert laden; unboxen
            ilGenerator.Emit(OpCodes.Ldarg_1);
            ilGenerator.Emit(OpCodes.Castclass,memberInfo.ReflectedType);
            ilGenerator.Emit(OpCodes.Ldarg_2);
            ilGenerator.Emit(OpCodes.Unbox_Any,memberType);

            if (propertyInfo!=null)
                // Property setzen
                ilGenerator.Emit(OpCodes.Callvirt,propertyInfo.GetSetMethod(true));
            else
                // Feld setzen
                ilGenerator.Emit(OpCodes.Stfld,fieldInfo);

            // fertig
            ilGenerator.Emit(OpCodes.Ret);

            // falls gewünscht, hier der Teil zum Konvertieren
            if (changeType)
            {
                ilGenerator.MarkLabel(labelSetConverted);

                // Zielobjekt laden und casten
                ilGenerator.Emit(OpCodes.Ldarg_1);
                ilGenerator.Emit(OpCodes.Castclass,memberInfo.ReflectedType);

                if (!memberType.IsEnum)
                {
                    // value laden
                    ilGenerator.Emit(OpCodes.Ldarg_2);

                    MethodInfo convertToMethodInfo = DynamicFieldAccessor.getConvertToMethodInfo(memberType);
                    if (convertToMethodInfo!=null)
                        // {Convert.ToXY(value)}
                        ilGenerator.Emit(OpCodes.Call,convertToMethodInfo);
                    else
                    {
                        // {Convert.ChangeType(value,typeof(MemberType))}
                        // Zieltyp laden
                        ilGenerator.Emit(OpCodes.Ldtoken,memberType);
                        ilGenerator.Emit(OpCodes.Call,typeGetTypeFromHandleMethodInfo);
                        if (memberType.IsGenericType && memberType.GetGenericTypeDefinition()==typeof(Nullable<>))
                            // {Convert.ChangeType(value,Nullable.GetUnderlyingType(typeof(MemberType)))}
                            // GetUnderlyingType aufrufen
                            ilGenerator.Emit(OpCodes.Call,nullableGetUnderlyingTypeMethodInfo);
                        // .ToXY/.ChangeType aufrufen
                        ilGenerator.Emit(OpCodes.Call,convertChangeTypeMethodInfo);
                        ilGenerator.Emit(OpCodes.Unbox_Any,memberType);
                    }
                }
                // Enums
                else
                {
                    // Enum-Zieltyp laden
                    ilGenerator.Emit(OpCodes.Ldtoken,memberType);
                    ilGenerator.Emit(OpCodes.Call,typeGetTypeFromHandleMethodInfo);
                    // value laden und zu string machen
                    ilGenerator.Emit(OpCodes.Ldarg_2);
                    ilGenerator.Emit(OpCodes.Callvirt,objectToStringMethodInfo);
                    // {Enum.Parse(typeof(MemberType),value)}
                    ilGenerator.Emit(OpCodes.Call,enumParseMethodInfo);
                    ilGenerator.Emit(OpCodes.Unbox_Any,memberType);
                }

                if (propertyInfo!=null)
                    // Property setzen
                    ilGenerator.Emit(OpCodes.Callvirt,propertyInfo.GetSetMethod(true));
                else
                    // Feld setzen
                    ilGenerator.Emit(OpCodes.Stfld,fieldInfo);

                // fertig
                ilGenerator.Emit(OpCodes.Ret);
            }
        }
    }

    /// <summary>
    /// Gibt das MethodInfo der zum übergebenen Typ gehörenden Convert.ToXY-Method zurück (null, falls keine passende gefunden)
    /// </summary>
    /// <param name="targetType">Typ, in den konvertiert werden soll</param>
    /// <returns>das MethodInfo der zum übergebenen Typ gehörenden Convert.ToXY-Method zurück (null, falls keine passende gefunden)</returns>
    private static MethodInfo getConvertToMethodInfo(Type targetType)
    {
        // Signatur der gesuchen Methode ermitteln
        string methodName;
        if (targetType==typeof(Boolean))
            methodName = "ToBoolean";
        else if (targetType==typeof(Byte))
            methodName = "ToByte";
        else if (targetType==typeof(Char))
            methodName = "ToChar";
        else if (targetType==typeof(DateTime))
            methodName = "ToDateTime";
        else if (targetType==typeof(Decimal))
            methodName = "ToDecimal";
        else if (targetType==typeof(Double))
            methodName = "ToDouble";
        else if (targetType==typeof(Int16))
            methodName = "ToInt16";
        else if (targetType==typeof(Int32))
            methodName = "ToInt32";
        else if (targetType==typeof(Int64))
            methodName = "ToInt64";
        else if (targetType==typeof(SByte))
            methodName = "ToSByte";
        else if (targetType==typeof(Single))
            methodName = "ToSingle";
        else if (targetType==typeof(String))
            methodName = "ToString";
        else if (targetType==typeof(UInt16))
            methodName = "ToUInt16";
        else if (targetType==typeof(UInt32))
            methodName = "ToUInt32";
        else if (targetType==typeof(UInt64))
            methodName = "ToUInt64";
        else
            return null;

        // Methode mit der entsprechenden Signatur suchen und zurückgeben
        return typeof(Convert).GetMethod(methodName,new Type[]{typeof(object)});
    }
}
